/*
  S.M.A.C.K - An operating system kernel
  Copyright (C) 2010,2011 Mattias Holm and Kristian Rietveld
  For licensing and a full list of authors of the kernel, see the files
  COPYING and AUTHORS.
*/

#include <vm.h>
#include <stdio.h>
#include <string.h>
#include <device.h>
#include <omap-clock.h>
#include <omap-gpio.h>
#include <errno.h>
#include <ioctl.h>

struct dss
{
  uint32_t dss_revisionnumber;
  uint32_t padding0[3];
  uint32_t dss_sysconfig;
  uint32_t dss_sysstatus;
  uint32_t dss_irqstatus;
  uint32_t padding1[9];
  uint32_t dss_control;
  uint32_t dss_sdi_control;
  uint32_t dss_pll_control;
  uint32_t padding2[4];
  uint32_t dss_sdi_status;
  uint32_t padding3[232];
  uint32_t dispc_revision;
  uint32_t padding4[3];
  uint32_t dispc_sysconfig;
  uint32_t dispc_sysstatus;
  uint32_t dispc_irqstatus;
  uint32_t dispc_irqenable;
  uint32_t padding5[8];
  uint32_t dispc_control;
  uint32_t dispc_config;
  uint32_t padding6[1];
  uint32_t dispc_default_color[2];
  uint32_t dispc_trans_color[2];
  uint32_t dispc_line_status;
  uint32_t dispc_line_number;
  uint32_t dispc_timing_h;
  uint32_t dispc_timing_v;
  uint32_t dispc_pol_freq;
  uint32_t dispc_divisor;
  uint32_t dispc_global_alpha;
  uint32_t dispc_size_dig;
  uint32_t dispc_size_lcd;
  uint32_t dispc_gfx_ba[2];
  uint32_t dispc_gfx_position;
  uint32_t dispc_gfx_size;
  uint32_t padding7[4];
  uint32_t dispc_gfx_attributes;
  uint32_t dispc_gfx_fifo_threshold;
  uint32_t dispc_gfx_fifo_size_status;
  uint32_t dispc_gfx_row_inc;
  uint32_t dispc_gfx_pixel_inc;
  uint32_t dispc_gfx_window_skip;
  uint32_t dispc_gfx_table_ba;
};

static volatile struct dss *dss;
static uint8_t *framebuffer = NULL;
static uint8_t *framebuffer_rw_head = NULL;
static uint16_t framebuffer_width = 0;
static uint16_t framebuffer_height = 0;
static uint32_t framebuffer_size = 0;

#define DSS_CONTROL_DISPC_CLK_SWITCH 0

#define DSS_SYSCONFIG_SOFTRESET 1

#define DSS_SYSSTATUS_RESETDONE 0

#define DISPC_IRQSTATUS_FRAMEDONE 0

#define DISPC_CONTROL_LCDENABLE 0
#define DISPC_CONTROL_STNTFT 3
#define DISPC_CONTROL_GOLCD 5
#define DISPC_CONTROL_TFTDATALINES 8

#define DISPC_CONFIG_LOADMODE 1

#define DISPC_GFX_ATTRIBUTES_GFXENABLE 0
#define DISPC_GFX_ATTRIBUTES_GFXFORMAT 1

#define DISPC_GFX_SIZE_GFXSIZEX 0
#define DISPC_GFX_SIZE_GFXSIZEY 16

#define DISPC_SIZE_LCD_PPL 0
#define DISPC_SIZE_LCD_LPP 16

static void
video_init_with_resolution(int width, int height)
{
  unsigned int tmp;
  unsigned int lcd_enabled;

  dss = vm_map_physical(vm_get_kernel_map(VM_REG_DEVICE), VM_SUPER_RW | VM_DEVICE,
                        (uintptr_t)0x48050000, 4096);

  framebuffer_width = width;
  framebuffer_height = height;
  /* We default to RGB16 (5R6G5B) as pixel format: 2 bytes per pixel. */
  framebuffer_size = width * height * 2;

  // Device flag needed to get physically consecutive memory
  framebuffer = vm_map(vm_get_kernel_map(VM_REG_DEVICE), VM_SUPER_RW | VM_DEVICE, 0,
                       (framebuffer_size / 4096 + 1) * 4096);
  framebuffer_rw_head = framebuffer;

  /* Clear frame buffer area */
  memset(framebuffer, 0, width * height * 2);

  /* Reset display subsystem (OMAP manual, section 15.5.1) */
  tmp = dss->dispc_control;

  /* FIXME: Currently for the hardware, we rely on the boot loader
   * correctly initializing the display subsystem.  We should fix the
   * driver in the future to be able to property initialize the system.
   * The initialization that's here right now is just enough to get qemu
   * to display the framebuffer.
   */
  if ((tmp & (1 << DISPC_CONTROL_LCDENABLE)) == (1 << DISPC_CONTROL_LCDENABLE))
    lcd_enabled = 1;
  else
    lcd_enabled = 0;

  if (!lcd_enabled)
    {
      /* FIXME: This code is only needed when the LCD is currently
       * enabled and want to disable it or prepare for reset.
       */
#if 0
      tmp = dss->dispc_control;
      tmp &= ~(1 << DISPC_CONTROL_LCDENABLE);
      dss->dispc_control = tmp;

      tmp = dss->dispc_irqstatus;
      tmp |= 1 << DISPC_IRQSTATUS_FRAMEDONE;
      dss->dispc_irqstatus = tmp;

      /* Wait until end of frame, which means LCD stop is complete */
      while ((dss->dispc_irqstatus & (1 << DISPC_IRQSTATUS_FRAMEDONE)) != (1 << DISPC_IRQSTATUS_FRAMEDONE));
#endif
    }

  /* Take display subsystem out of reset by enabling necessary clocks */
  omap_clock_enable_dss_clocks ();

  if (!lcd_enabled)
    {
      /* Soft reset, wait for resetdone line to become high */
      dss->dss_sysconfig |= 1 << DSS_SYSCONFIG_SOFTRESET;

      while ((dss->dss_sysstatus & (1 << DSS_SYSSTATUS_RESETDONE)) != (1 << DSS_SYSSTATUS_RESETDONE));

      /* Display subsystem configuration (OMAP manual, section 15.5.2) */
      dss->dss_control |= 1 << DSS_CONTROL_DISPC_CLK_SWITCH;

      /* Program display controller (OMAP manual, section 15.5.3) */
      dss->dispc_config |= 0x2 << DISPC_CONFIG_LOADMODE;

      /* FIXME: This code is also needed below for the case the LCD is
       * already enabled by the bootloader.  When the LCD size is modified,
       * the H and V timings will very likely have to be updated as well.
       */
      tmp = (height - 1) << DISPC_SIZE_LCD_LPP;
      tmp |= (width - 1) << DISPC_SIZE_LCD_PPL;
      dss->dispc_size_lcd = tmp;
    }

  tmp = dss->dispc_control;
  tmp &= ~(1 << DISPC_CONTROL_GOLCD);
  dss->dispc_control = tmp;

  tmp = dss->dispc_gfx_attributes;
  tmp |= 1 << DISPC_GFX_ATTRIBUTES_GFXENABLE;
  tmp |= 0x6 << DISPC_GFX_ATTRIBUTES_GFXFORMAT;
  dss->dispc_gfx_attributes = tmp;

  dss->dispc_gfx_ba[0] = vm_probe_physical(NULL, framebuffer);
  dss->dispc_gfx_ba[1] = vm_probe_physical(NULL, framebuffer);

  dss->dispc_gfx_position = 0x0;

  tmp = (height - 1) << DISPC_GFX_SIZE_GFXSIZEY;
  tmp |= (width - 1) << DISPC_GFX_SIZE_GFXSIZEX;
  dss->dispc_gfx_size = tmp;

  dss->dispc_default_color[0] = 0x000000;

  tmp = dss->dispc_control;
  tmp |= 1 << DISPC_CONTROL_GOLCD;
  tmp |= 1 << DISPC_CONTROL_LCDENABLE;
  tmp |= 0x3 << DISPC_CONTROL_TFTDATALINES;
  tmp |= 1 << DISPC_CONTROL_STNTFT;
  dss->dispc_control = tmp;

  omap_gpio_set_dvid(1);
}


size_t
framebuf_read(dev_t *dev, dev_minor_t minor, void *buff, size_t nbytes)
{
  if (minor != 0)
    return -1;

  if ((uintptr_t)framebuffer_rw_head + nbytes
      < (uintptr_t)framebuffer + framebuffer_size)
    {
      memcpy(buff, framebuffer_rw_head, nbytes);
      framebuffer_rw_head += nbytes;
      return nbytes;
    }
  else if (nbytes < framebuffer_size)
    {
      /* Read to the end, then wrap around and read from the first */
      uintptr_t to_end = framebuffer_size - (uintptr_t)framebuffer_rw_head;

      memcpy(buff, framebuffer_rw_head, to_end);
      framebuffer_rw_head = framebuffer;

      memcpy(buff, framebuffer_rw_head, nbytes - to_end);
      framebuffer_rw_head += nbytes - to_end;

      return nbytes;
    }
  /* else: FIXME: support wrapping around multiple times.  But who
   * would want to do that anyway?
   */

  return -1;
}

size_t
framebuf_write(dev_t *dev, dev_minor_t minor, const void *buff, size_t nbytes)
{
  if (minor != 0)
    return -1;

  if ((uintptr_t)framebuffer_rw_head + nbytes
      < (uintptr_t)framebuffer + framebuffer_size)
    {
      memcpy(framebuffer_rw_head, buff, nbytes);
      framebuffer_rw_head += nbytes;
      return nbytes;
    }
  else if (nbytes < framebuffer_size)
    {
      /* Read to the end, then wrap around and read from the first */
      uintptr_t to_end = framebuffer_size - (uintptr_t)framebuffer_rw_head;

      memcpy(framebuffer_rw_head, buff, to_end);
      framebuffer_rw_head = framebuffer;

      memcpy(framebuffer_rw_head, buff, nbytes - to_end);
      framebuffer_rw_head += nbytes - to_end;

      return nbytes;
    }
  /* else: FIXME: support wrapping around multiple times.  But who
   * would want to do that anyway?
   */

  return -1;
}

static void
framebuf_clear(dev_t *dev, dev_minor_t minor)
{
  if (minor != 0)
    return;

  memset(framebuffer, 0, framebuffer_size);
}

off_t
framebuf_seek(dev_t *dev, dev_minor_t minor, off_t offset, int whence)
{
  if (minor != 0)
    return -1;

  switch (whence)
    {
      case SEEK_SET:
        if (offset >= 0 && offset < framebuffer_size)
          {
            framebuffer_rw_head = framebuffer + offset;
            return offset;
          }
        else
          return -1;
        break;

      /* SEEK_CUR supports wrap around just like read/write */
      case SEEK_CUR:
        framebuffer_rw_head = framebuffer + (offset % framebuffer_size);
        return (off_t)framebuffer_rw_head - (off_t)framebuffer;
        break;

      case SEEK_END:
        if ((uintptr_t)framebuffer <= (uintptr_t)framebuffer_rw_head + offset
            && (uintptr_t)framebuffer_rw_head + offset < (uintptr_t)framebuffer + framebuffer_size)
          {
            framebuffer_rw_head = framebuffer + framebuffer_size - offset;
            return (off_t)framebuffer_rw_head - (off_t)framebuffer;
          }
        else
          return -1;
        break;
    }

  return -1;
}

int
framebuf_open(dev_t *dev, dev_minor_t minor)
{
  if (minor != 0)
    return -1;

  return 0;
}

int
framebuf_close(dev_t *dev, dev_minor_t minor)
{
  if (minor != 0)
    return -1;

  return 0;
}

int
framebuf_ioctl(dev_t *dev, dev_minor_t minor, uint32_t request, uint32_t param)
{
  if (minor != 0)
    return -1;

  switch (request)
    {
      case FB_CLEAR_BUFFER:
        framebuf_clear(dev, minor);
        break;

      default:
        return -EINVAL;
        break;
    }

  /* FIXME: ioctls that would be interesting to have:
   *   - set and get resolution
   *   - set and get pixel format
   *   - swap buffers
   */

  return 0;
}

static dev_ops_t framebuf_ops = {
  .read = framebuf_read,
  .write = framebuf_write,
  .seek = framebuf_seek,
  .open = framebuf_open,
  .close = framebuf_close,
  .ioctl = framebuf_ioctl
};

static dev_t framebuf_dev = {
  .type = DEV_FRAMEBUF,
  .name = "framebuf",
  .mountname = "fb",
  .major = DEV_FRAMEBUF,
  .minor_count = 1,
  .ops = &framebuf_ops
};


void
video_init(void)
{
  omap_gpio_init();

  puts ("BeagleBoard framebuffer driver initializing ...");
  video_init_with_resolution(800, 600);

  dev_register(DEV_FRAMEBUF, &framebuf_dev);
}
